/* 
 * [New BSD License (BSD 3-Clause License)]
 * Copyright (c) 2012, Max Bechtold
 * All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 * 
 *     - Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     - Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *     - Neither the name of the Rel2Xml Project nor the
 *       names of its contributors may be used to endorse or promote products
 *       derived from this software without specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package maxbe.rel2xml.generator.dml;

import static maxbe.rel2xml.util.Config.getBoolean;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import maxbe.rel2xml.generator.Column;
import maxbe.rel2xml.generator.Column.Mapping;
import maxbe.rel2xml.generator.Table;
import maxbe.rel2xml.util.Config;

public class Projection implements Clause {

	private final List<ResultColumn> columns;

	private Query query;

	public Projection(List<ResultColumn> columns) {
		this.columns = columns;
	}

	public List<ResultColumn> getColumns() {
		return columns;
	}

	public void setQuery(Query query) {
		this.query = query;
	}

	@Override
	public String toXQuery() {
		String project = "";

		String format = null;
		String entitySeparator = ",\n";

		if (!query.joinedHierarchically())
			format = "return <res>{\n%s\n}</res>";
		else
			format = "return {\n%s\n}";

		for (Entity entity : query.getEntities().values())
			if (isReturned(entity)
					&& (entity.getParent() == null || !isReturned(entity
							.getParent())))
				project += (project.isEmpty() ? "" : entitySeparator)
						+ project(entity, "\t");

		return String.format(format, project);
	}

	private String project(Entity entity, String indent) {
		String alias = '$' + entity.getAlias();

		if (!isProjected(entity)) {
			return indent + alias;
		} else if (!query.joinedHierarchically()
				&& !getBoolean(Config.PRESERVE_ENTITIES)) {
			// Create content expression according to constellation
			String subList = "";

			for (ResultColumn column : columns)
				// May be an empty list, but table could be an intermediate step
				// in a hierarchy
				if (column.getEntity() == entity) {
					if (column.getName().equals("*")) {
						for (Column col : column.getEntity().getTable()
								.getColumns()) {
							if (subList.length() > 0)
								subList += ",\n";
							subList += indent + formatAttribute(col, alias);
						}
					} else {
						if (subList.length() > 0)
							subList += ",\n";
						subList += indent
								+ formatAttribute(column.getColumn(), alias);
					}
				}

			for (Entity child : entity.getChildren())
				if (isReturned(child))
					subList += (subList.length() > 0 ? ",\n" : "")
							+ project(child, indent + '\t');

			return subList;
		} else {
			return getModifications(entity, indent);
		}
	}

	private String getModifications(Entity entity, String indent) {
		// Project using transforms

		String transform = getDeleteExpressions(entity, "$new", indent);
		String lineBreak = String.format("\n%s\t", indent);
		String sep = "," + lineBreak;
		List<Table> docChildren = new ArrayList<Table>(entity.getTable()
				.getChildren());

		for (Entity child : entity.getChildren()) {
			boolean childInDoc = docChildren.remove(child.getTable());

			if (!isReturned(child)) {
				transform += (!transform.isEmpty() ? sep : "")
						+ String.format("delete node $new/%ss", child
								.getTable().getName());
			} else {
				if (!child.isRemapped()) {
					transform += (!transform.isEmpty() ? sep : "")
							+ getDeleteExpressions(child, "$new"
									+ child.getTable().getXPathSelector(true),
									indent);
				} else {
					if (childInDoc)
						transform += (!transform.isEmpty() ? sep : "")
								+ String.format("delete nodes $new/%ss/%s",
										child.getTable().getName(), child
												.getTable().getName());
					else
						transform += (!transform.isEmpty() ? sep : "")
								+ String.format("insert node <%ss/> into $new",
										child.getTable().getName());

					String newNode = null;
					if (isProjected(child))
						newNode = getModifications(child, indent + "\t\t");
					else
						newNode = project(child, indent + "\t\t");

					transform += String.format(
							"%sinsert nodes\n%s%sinto $new/%ss", sep, newNode,
							lineBreak, child.getTable().getName());
				}
			}
		}

		for (Table table : docChildren) {
			transform += (!transform.isEmpty() ? sep : "")
					+ String.format("delete node $new/%ss", table.getName());
		}

		return String.format(
				"%scopy $new := $%s\n%smodify (\n%s\t%s\n%s)\n%sreturn $new",
				indent, entity.getAlias(), indent, indent, transform, indent,
				indent);
	}

	private static String formatAttribute(Column column, String tableAlias) {
		if (column.getMapping() == Mapping.XML_ATTRIBUTE)
			return tableAlias + '/' + column.getXmlName();
		else
			return String.format("attribute {'%s'} {fn:data(%s/%s)}",
					column.getName(), tableAlias, column.getXmlName());
	}

	private String getDeleteExpressions(Entity entity, String path,
			String indent) {
		String delete = "";
		List<Column> pruned = new ArrayList<Column>(entity.getTable()
				.getColumns());
		String sep = String.format(",\n%s\t", indent);

		for (ResultColumn column : columns) {
			if (column.getEntity() == entity)
				if (column.getName().equals("*"))
					pruned.clear();
				else
					pruned.remove(column.getColumn());
		}

		for (Column column : pruned) {
			if (!delete.isEmpty())
				delete += sep;
			delete += String.format("delete node %s/%s", path,
					column.getXmlName());
		}

		return delete;
	}

	/**
	 * Checks whether a projection is to be made on an entity or if all of its
	 * columns are part of the result. This includes several checks on this
	 * entity's children
	 * 
	 * @param entity
	 * @return
	 */
	boolean isProjected(Entity entity) {
		if (!query.joinedHierarchically()
				&& !getBoolean(Config.PRESERVE_ENTITIES))
			return true;

		boolean projected = false;
		List<Column> allColumns = new ArrayList<Column>(entity.getTable()
				.getColumns());
		for (ResultColumn column : columns)
			if (column.getEntity() == entity) {
				if ("*".equals(column.getName())) {
					if (allColumns.size() != entity.getTable().getColumns()
							.size()) {
						projected = true;
						break;
					}
					allColumns.clear();
				} else if (allColumns.isEmpty()) {
					projected = true;
					break;
				} else {
					allColumns.remove(column.getColumn());
				}
			}

		if (!projected && !allColumns.isEmpty())
			projected = true;

		List<Table> docChildren = entity.getTable().getChildren();
		Iterator<Table> docChildrenIt = docChildren.iterator();

		if (!projected)
			// The output hierarchy has to be the same as the document hierarchy
			if (!query.joinedHierarchically()) {
				if (docChildren.size() > 0)
					projected = true;
			} else if (docChildren.size() != entity.getChildren().size()) {
				projected = true;
			} else {
				for (Entity child : entity.getChildren()) {
					projected = (docChildrenIt.next() != child.getTable()
							|| isProjected(child) || child.isRemapped());
					if (projected)
						break;
				}
			}

		return projected;
	}

	/**
	 * Checks whether an entity is part of the projection
	 * 
	 * @param entity
	 * @return
	 */
	boolean isReturned(Entity entity) {
		for (ResultColumn column : columns)
			if (column.getEntity() == entity)
				return true;
		return false;
	}

	@Override
	public String toSql() {
		String result = "select ";

		for (ResultColumn column : columns) {
			result += column.getName() + ", ";
		}

		return result.substring(0, result.length() - 2);
	}

	@Override
	public String toString() {
		return toSql();
	}
}
